﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SmartRoute.Data
{
    [Serializable]
    public class Route
    {

        #region Constants

        private const string incorrectDeserializationErrorString =
            "Route: Constructed (probably deserialized) without providing a valid routing service";

        #endregion

        #region Members

        private Location _source;
        private Location _destination;
        private TimeSpan _duration;
        private double _length;
        private Location[] _waypoints;
        private string _directions;


        [NonSerialized]
        private IRoutingService _routingService = new ErrorRoutingService(incorrectDeserializationErrorString);

        #endregion
        
        #region Accessors

        /// <summary>
        /// The destination of this route.
        /// </summary>
        public Location Destination
        {
            get { return _destination; }
        }

        /// <summary>
        /// Directions for this route.
        /// </summary>
        public string Directions
        {
            get { return _directions; }
            set { _directions = value; }
        }

        /// <summary>
        /// The source of this route.
        /// </summary>
        public Location Source
        {
            get { return _source; }
        }

        /// <summary>
        /// The time it takes to finish this route.
        /// </summary>
        public TimeSpan Duration
        {
            get { return _duration; }
        }

        /// <summary>
        /// The distance travelled on this route.
        /// </summary>
        public double Distance
        {
            get { return _length; }
        }

        /// <summary>
        /// The intermediate locations visited on this route.
        /// </summary>
        public IEnumerable<Location> Waypoints
        {
            get { return _waypoints; }
        }
        
        /// <summary>
        /// The mutable reference to the routing service that generated this route.
        /// Only routing services should change this pointer.
        /// </summary>
        public IRoutingService RoutingService
        {
            get { return _routingService; }
            set { _routingService = value; }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Builds a new route object that does not go anywhere but takes a fixed amount of time.
        /// </summary>
        /// <param name="toIdleAt">The place to idle at.</param>
        /// <param name="timeToIdle">The time to idle at the location "toIdleAt".</param>
        /// <param name="routingService">The reference to the routing service to be used.</param>
        /// <returns>A route that goes from its source to its source, has no distance, but takes some time.</returns>
        public static Route IdleInPlace(Location toIdleAt, TimeSpan timeToIdle, IRoutingService routingService)
        {
            Route outp = new Route();
            outp._length = 0;
            outp._duration = timeToIdle;
            outp._source = toIdleAt;
            outp._destination = toIdleAt;
            outp._waypoints = new Location[] { toIdleAt };
            outp._routingService = routingService;
            return outp;
        }

        /// <summary>
        /// Constructs a new route. Intended to be called by the routing service's FromTo method.
        /// </summary>
        /// <param name="routingService">The pointer to the routing service that constructed this object.</param>
        /// <param name="source">Where this route begins.</param>
        /// <param name="destination">Where this route ends.</param>
        /// <param name="travelTime">How long it takes to finish this route.</param>
        /// <param name="distance">How much distance is travelled on this route.</param>
        /// <param name="waypoints">Which locations are visited on the route.</param>
        public Route(IRoutingService routingService, Location source, Location destination, TimeSpan travelTime,
            double distance, Location[] waypoints, string directions)
        {
            this._routingService = routingService;
            this._source = source;
            this._waypoints = (Location[])waypoints.Clone();
            this._duration = travelTime;
            this._length = distance;
            this._destination = destination;
            this._directions = directions;
        }

        /// <summary>
        /// Allows extension of this class
        /// </summary>
        protected Route() { }

        #endregion

        #region Routing Services

        /// <summary>
        /// Generates a route that connects the first route to the second, visiting all waypoints.
        /// The routing service used is the routing service used by the first argument.
        /// </summary>
        /// <param name="first">The first route to run through before moving to the second</param>
        /// <param name="second">The route to run through after moving through the first</param>
        /// <returns>A route that runs through both argument routes.</returns>
        public static Route Concatenate(Route first, Route second)
        {
            //These two routes are connected. Simply add them together
            if (first.Destination == second.Source)
            {
                double totalDistance = first.Distance + second.Distance;
                TimeSpan totalTime = first.Duration + second.Duration;
                string newDirections = 
                    (first.Directions != null ? first._directions.Trim() + System.Environment.NewLine + " " + System.Environment.NewLine : "")
                    + (second.Directions != null ? second._directions.Trim() : "");
                Location[] newWaypoints = ConcatWaypoints(first, second).ToArray<Location>();
                return new Route(first.RoutingService, first.Source, second.Destination, totalTime, totalDistance, newWaypoints, newDirections);
            }
            //These two routes are not connected. Have routing services connect them first.
            else
            {
                Route intermediate = first.RoutingService.FromTo(first.Destination, second.Source);
                if (intermediate.Source != first.Destination) throw new InvalidOperationException
                     ("Route.Concatenate: Routing service for first route returned bogus result (source route does not lead to intermediate route)");
                if (intermediate.Destination != second.Source) throw new InvalidOperationException
                     ("Route.Concatenate: Routing service for first route returned bogus result (intermediate route does not lead to destination route)");
                return (Concatenate(Concatenate(first,intermediate),second));
            }
        }

        private static IEnumerable<Location> ConcatWaypoints(Route first, Route second)
        {
            foreach (Location waypoint in first._waypoints) yield return waypoint;
            yield return first.Destination;
            foreach (Location waypoint in second._waypoints) yield return waypoint;
        }

        #endregion

        #region Equality

        /// <summary>
        /// Returns true only if the other object is a route with the same starting, ending, and waypoints
        /// </summary>
        /// <param name="obj">The object to be tested for equality with this one.</param>
        /// <returns>True if and only if argument object is the same route.</returns>
        public override bool Equals(object obj)
        {
            return obj is Route 
                && ((Route)obj).Destination == this.Destination
                && ((Route)obj).Source == this.Source
                && ((Route)obj).waypointsEquivalent(this);
        }

        //Test to see if the waypoints are equivalent to one another.
        private bool waypointsEquivalent(Route other)
        {
            if (other._waypoints.Length != this._waypoints.Length) return false;
            else for (int i = 0; i < this._waypoints.Length; i++)
                {
                    if (this._waypoints[i] != other._waypoints[i]) return false;
                }
            return true;
        }

        public override int GetHashCode()
        {
            unchecked
            {
                //Jenkins' one-at-a-time hash algorithm
                int hash = this.Source.GetHashCode();
                hash += (hash << 10);
                hash ^= (hash >> 6);
                hash += this.Destination.GetHashCode();
                hash += (hash << 10);
                hash ^= (hash >> 6);
                foreach (Location waypoint in this._waypoints)
                {
                    hash += waypoint.GetHashCode();
                    hash += (hash << 10);
                    hash ^= (hash >> 6);
                }
                hash += (hash << 3);
                hash ^= (hash >> 11);
                hash += (hash << 15);
                return hash;
            }
        }

        #endregion

        #region Operator Overloads

        public static Route operator +(Route source, Route destination)
        {
            return Concatenate(source, destination);
        }
        public static Route operator +(Location source, Route destination)
        {
            return Route.IdleInPlace(source, TimeSpan.Zero, destination.RoutingService) + destination;
        }
        public static Route operator +(Route source, Location destination)
        {
            return source + Route.IdleInPlace(destination, TimeSpan.Zero, source.RoutingService);
        }
        public static bool operator ==(Route first, Route second)
        {
            return first.Equals(second);
        }
        public static bool operator !=(Route first, Route second)
        {
            return !(first == second);
        }

        #endregion

    }
}
